【实战】设计模式应用之策略模式

前言

关于设计模式的文章,园子里实在是太多太多,而且讲解的也非常精彩,那为什么我还要在这里记录下这篇文章?本文以实际项目应用“自己动手写工具--XSmartNote”为切入点,来讲述策略模式的应用。很多初学者都有一种感觉,就是在看设计模式相关文章的时候,都看得懂,而且小Demo也是手到擒来,但是就是不知道该怎么用在实际的项目中,不管你之前有没有过这种感觉,反正我是曾经有过。在前几天Review Code的时候发现XSmartNote中的主题管理功能很适合这种模式,于是就把这块相关的代码重构了一下。在此做一下记录,一来方便自己,二来惠及他人。

策略模式

策略模式的用意是针对一组算法或逻辑,将每一个算法或逻辑封装到具有共同接口的独立的类中,从而使得它们之间可以相互替换。策略模式使得算法或逻辑可以在不影响到客户端的情况下发生变化。说到策略模式就不得不提及OCP(Open Closed Principle) 开闭原则,即对扩展开放,对修改关闭。策略模式的出现很好地诠释了开闭原则,有效地减少了分支语句。

应用场景

下面来说说策略模式的应用场景,以下引自百度百科:

1、 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
2、 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
3、 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。

代码框架

下面还是从一个生活中的小例子入手,解释一下策略模式的大概用法,深入浅出地理解这个常用的设计模式。

假设老板有一天突然对办公室所有的程序员说,“给你们20天假期,去海南玩吧,经费从我这出!”,这时候办公室躁动了,哈哈,大家开始商量着怎么去海南,毕竟我们还在帝都呀,毕竟我还没有去过海南呀,大家七嘴八舌地出起主意来,有人说坐灰机,也有人说坐动车转海路... ...好了,上面只是一个业务场景,不要想太多了。那怎么实现呢?直接上策略模式,代码如下:

首先定义一个接口,ITravel包含了一个无返回值的Travel方法

1 interface ITravel
2 {
3     void Travel();
4 }

然后建立一个维护Travel的上下文,这里应用了单例模式产生TravelContext类,并包含了SetTravel方法用于设置Travel策略,以及Travel方法用于执行策略。代码如下:

 1 class TravelContext
 2 {
 3     private ITravel _travel=null;
 4     private static TravelContext _travelContext;
 5     private static object lockObj=new object();
 6 
 7     private TravelContext(ITravel travel)
 8     {
 9         this._travel = travel;
10     }
11 
12     public static TravelContext CreateTravelContext(ITravel travel)
13     {
14         if (null==_travelContext)
15         {
16             lock (lockObj)
17             {
18                 if (null == _travelContext)
19                 {
20                     _travelContext = new TravelContext(travel);
21                 }
22             }
23         }
24         return _travelContext;
25     }
26 
27     public void SetTravel(ITravel setTravel)
28     {
29         this._travel = setTravel;
30     }
31 
32     public void Travel()
33     {
34         this._travel.Travel();
35     }
36 }

建立好上下文后,开始建立具体的策略方案,本例中就是几种Travel的方式,不管以哪种方式执行策略,我们都是在旅行,所以每种策略都实现ITravel接口,并具体实现ITravel接口下的Travel方法,代码如下:

 1 class PlainTravel:ITravel
 2 {
 3     public void Travel()
 4     {
 5         Console.WriteLine("我们老大掏腰包,打飞机去海南!");
 6     }
 7 }
 8 
 9 class BusTravel:ITravel
10 {
11     public void Travel()
12     {
13         Console.WriteLine("我们老大掏腰包,坐汽车去海南!");
14     }
15 }
16 
17 class BikeTravel : ITravel
18 {
19     public void Travel()
20     {
21         Console.WriteLine("我们老大没钱了,骑自行车去海南!");
22     }
23 }

下面来看看客户端如何使用上述旅行的策略模式,代码如下:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         TravelContext context = TravelContext.CreateTravelContext(new PlainTravel());//打飞机去海南
 6         context.Travel();//飞起来
 7         context.SetTravel(new BusTravel());//飞机没油了,坐汽车吧
 8         context.Travel();//跑起来
 9         context.SetTravel(new BikeTravel());//汽车轮胎扎了... ...骑车去吧
10         context.Travel();//走你
11         Console.ReadLine();
12     }
13 }

以上只是一个简单的例子,没有什么实际的意义,也不是很切题,那为什么还要写出来?只是让我们对策略模式的构成以及应用场景有一个大概的认识,下面我会根据代码重构的经历来说说策略模式在具体应用程序中的应用。

策略模式的实践

在我之前的一篇博文XSmartNote里,有这样的一个功能,就是切换应用的配色方案,当我选择不同的配色方案时,会执行Switch语句中相应的方案来达到修改配色方案的目的。下面用代码来解释这个过程:

 1 public void SetTheme(ThemeManager.ThemeEnums.ThemeEnum enums)
 2 {
 3     switch (enums)
 4     {
 5         case ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_MISTYROSE:
 6             this.BackgroundImage = Resources.bg_10_03;
 7             SetThemeColor(Color.MistyRose);
 8             SetTextAndBarColor(Color.Maroon, Color.Silver);
 9             this.Invalidate();
10             break;
11         case ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_ALICEBLUE:
12             this.BackgroundImage = Resources.bg_10_04;
13             SetThemeColor(Color.AliceBlue);
14             SetTextAndBarColor(Color.LightBlue, Color.Black);
15             break;
16         case ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_HONEYDEW:
17             this.BackgroundImage = Resources.bg_10_02;
18             SetThemeColor(Color.Honeydew);
19             SetTextAndBarColor(Color.LightGreen, Color.Black);
20             break;
21         case ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_LEMONCHIFFON:
22             this.BackgroundImage = Resources.bg_10_01;
23             SetThemeColor(Color.LemonChiffon);
24             SetTextAndBarColor(Color.Orange, Color.Black);
25             break;
26         default:
27             break;
28     }
29 }

上述代码中的两个方法SetThemeColorSetTextAndBarColor是设置配色方案的主要代码,传入的参数就是Color,然后这两个方法就会变更自己负责的部分的配色方案。下面是具体实现代码:

 1 private void SetThemeColor(Color color)
 2 {
 3     this.panel_Main.BackColor = color;
 4     this.tv_Folder.BackColor = color;
 5     this.txt_Title.BoxBackColor = color;
 6     this.txt_Content.BoxBackColor = color;
 7 }
 8 
 9 private void SetTextAndBarColor(Color color, Color tColor)
10 {
11     this.menuStripHeader.BackColor = color;
12     this.menuStripHeader.ForeColor = tColor;
13     foreach (ToolStripMenuItem item in this.menuStripHeader.Items)
14     {
15         item.ForeColor = tColor;
16     }
17 }

那么什么时候才会去调用这两个方法以实现配色方案的变更呢?当然是点击切换主题的时候,代码如下:

 1 private void roseRed_Click(object sender, EventArgs e)
 2 {
 3     ThemeManager.IThemeManager manager = ThemeManager.ThemeManager.CreateThemeManager(this);
 4     manager.ChangeFormTheme(ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_MISTYROSE);
 5 }
 6 
 7 private void stoneBlue_Click(object sender, EventArgs e)
 8 {
 9     ThemeManager.IThemeManager manager = ThemeManager.ThemeManager.CreateThemeManager(this);
10     manager.ChangeFormTheme(ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_ALICEBLUE);
11 }

上述代码中有一个ThemeManager类负责维护配色方案的功能,接收一个枚举ThemeEnum来确定要使用哪种配色方案,为了看得更方便,我把ThemeManager类的部分代码也放在这:

 1 public class ThemeManager:IThemeManager
 2 {
 3     private static ThemeManager themeManager;
 4     private MainForm mainForm;
 5     private event Action<ThemeEnums.ThemeEnum> ThemeChangeEvent;
 6     private static object _lock = new object();
 7     private ThemeManager(MainForm mainForm)
 8     {
 9         this.mainForm = mainForm;
10         ThemeChangeEvent += mainForm.SetTheme;
11     }
12 
13     public static ThemeManager CreateThemeManager(MainForm form)
14     {
15         ThemeManager _themeManager;
16         if (themeManager == null)
17         {
18             lock (_lock)
19             {
20                 if (themeManager == null)
21                 {
22                     _themeManager = new ThemeManager(form);
23                     themeManager = _themeManager;
24                 }
25             }
26         }
27         return themeManager;
28     }
29 
30     public void ChangeFormTheme(ThemeEnums.ThemeEnum enums)
31     {
32         if (ThemeChangeEvent != null)
33         {
34             ThemeChangeEvent(enums);
35         }
36     }
37 }

ThemeManager类的构造函数中绑定了主窗体中的SetTheme方法,也就是我上面贴出的第一段代码,并在ChangeFormTheme执行的时候调用。到此为止,这一块的功能大致上就OK啦,但是细心的你可能会发现,如果我又添加了一个配色方案怎么办?由上面的代码段可以看出,需要再添加一个枚举和一个Switch语句分支,问题就出在这里!!!如果要添加10个怎么办?20个呢?难道要一直修改Switch语句?很明显,这违背了OCP原则,即对扩展开放,对修改关闭的原则。这时该我们的策略模式上场了,下面是我重构以后的代码:

首先,建立一个接口ITheme,包含一个SetTheme方法。

1 public interface ITheme
2 {
3     void SetTheme();
4 }

再建立一个维护Theme的上下文,包含一个ITheme接口的引用和一个SetTheme方法,SetTheme方法中调用实现了ITheme接口的类的SetTheme方法。

public class ThemeContext 
{
    ITheme theme = null;

    public ThemeContext(ITheme myTheme)
    {
        theme = myTheme;
    }

    public void SetTheme()
    {
        theme.SetTheme();
    }
}

然后就是具体的实现策略,这里实现了具体的设置配色方案的逻辑。

 1 public class MistyRose : ITheme
 2 {
 3     private MainForm _Main;
 4     public MistyRose(MainForm main)
 5     {
 6         this._Main = main;
 7     }
 8     public void SetTheme()
 9     {
10         //实现主题设置
11         _Main.Panel_Main.BackColor = Color.MistyRose;
12         _Main.Tv_Folder.BackColor = Color.MistyRose;
13         _Main.Txt_Title.BoxBackColor = Color.MistyRose;
14         _Main.Txt_Content.BoxBackColor = Color.MistyRose;
15 
16         _Main.MenuStripHeader.BackColor = Color.Maroon;
17         _Main.MenuStripHeader.ForeColor = Color.Silver;
18         foreach (ToolStripMenuItem item in _Main.MenuStripHeader.Items)
19         {
20             item.ForeColor = Color.Silver;
21         }
22     }
23 }

下面再看看客户端是如何使用的。前两行是之前的调用方式,已经被注释掉了,最重要的是Switch语句不见了!!!

1 private void roseRed_Click(object sender, EventArgs e)
2 {
3     //ThemeManager.IThemeManager manager = ThemeManager.ThemeManager.CreateThemeManager(this);
4     //manager.ChangeFormTheme(ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_MISTYROSE);
5 
6     ITheme theme=new MistyRose(this);
7     ThemeContext themeContext = new ThemeContext(theme);
8     themeContext.SetTheme();
9 }

如果我想再添加一个主题配色方案该怎么办?很简单,添加一个类继承自ITheme并在客户端调用就好咯,代码如下:

 1 //添加一个新的配色方案
 2 public class AliceBlue : ITheme
 3 {
 4     private MainForm _Main;
 5     public AliceBlue(MainForm main)
 6     {
 7         this._Main = main;
 8     }
 9     public void SetTheme()
10     {
11         //实现主题设置
12         _Main.Panel_Main.BackColor = Color.AliceBlue;
13         _Main.Tv_Folder.BackColor = Color.AliceBlue;
14         _Main.Txt_Title.BoxBackColor = Color.AliceBlue;
15         _Main.Txt_Content.BoxBackColor = Color.AliceBlue;
16 
17         _Main.MenuStripHeader.BackColor = Color.LightBlue;
18         _Main.MenuStripHeader.ForeColor = Color.Black;
19         foreach (ToolStripMenuItem item in _Main.MenuStripHeader.Items)
20         {
21             item.ForeColor = Color.Black;
22         }
23     }
24 }
//客户端调用
private void stoneBlue_Click(object sender, EventArgs e)
{
    //ThemeManager.IThemeManager manager = ThemeManager.ThemeManager.CreateThemeManager(this);
    //manager.ChangeFormTheme(ThemeManager.ThemeEnums.ThemeEnum.KM_THEME_ALICEBLUE);

    ITheme theme = new AliceBlue(this);
    ThemeContext themeContext = new ThemeContext(theme);
    themeContext.SetTheme();
}

这样就完成了繁杂的Switch语句向策略模式的华丽转身,如果想看到具体的代码,请在GitHub上查看。最后放上简单的效果图:

总结

以上就是上次重构XSmartNote的过程,经过自己的思考和总结并实际运用到自己的小项目中,收获还是很大的,至少理解了策略模式在什么时候可以派上用场以及这种模式所解决的问题。可是有人会问,在客户端调用的时候,还是会new一个具体的对象啊,这样就会产生依赖,是的,这就是注入依赖要解决的问题咯,本文不做深入的探讨。如果文中有什么表述不当的地方,还请大家提出,谢谢大家,另外本文会同步发布到我的简书。 

posted @ 2016-06-01 09:03  悠扬的牧笛  阅读(8817)  评论(5编辑  收藏  举报